package middleware

import (
	"bytes"
	"go-mall/common/logger"
	"go-mall/common/util"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

func StartTrace() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		traceId := ctx.Request.Header.Get("traceid")
		pSpanId := ctx.Request.Header.Get("pspanid")
		spanId := util.GenerateSpanID(ctx.Request.RemoteAddr)
		if traceId == "" {
			// 如果traceId 为空，证明是链路的发端，把它设置成此次的spanId，发端的spanId是root spanId
			// trace 标识整个请求的链路, span则标识链路中的不同服务
			traceId = spanId
		}
		ctx.Set("traceid", traceId)
		ctx.Set("spanid", spanId)
		ctx.Set("pspanid", pSpanId)
		ctx.Next()
	}
}

type bodyLogWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

// 包装一下 gin.ResponseWriter，通过这种方式拦截写响应
// 让gin写响应的时候先写到 bodyLogWriter 再写gin.ResponseWriter ，
// 这样利用中间件里输出访问日志时就能拿到响应了
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin

func (w bodyLogWriter) Write(b []byte) (int, error) {
	w.body.Write(b)
	return w.ResponseWriter.Write(b)
}

// LogAccess 函数返回一个 gin.HandlerFunc，用于记录访问日志
func LogAccess() gin.HandlerFunc {
	// 返回一个匿名函数，该函数接收一个 *gin.Context 参数
	return func(ctx *gin.Context) {
		// 读取请求体
		reqBody, _ := io.ReadAll(ctx.Request.Body)
		// 将请求体重置为可读状态
		ctx.Request.Body = io.NopCloser(bytes.NewReader(reqBody))
		// 记录开始时间
		start := time.Now()
		// 创建一个 bodyLogWriter，用于记录响应体
		blw := &bodyLogWriter{
			body:           bytes.NewBufferString(""),
			ResponseWriter: ctx.Writer,
		}
		// 将 ctx.Writer 替换为 bodyLogWriter
		ctx.Writer = blw
		// 记录访问开始日志
		accessLog(ctx, "access_start", time.Since(start), reqBody, nil)
		// 在函数结束时记录访问结束日志
		defer func() {
			accessLog(ctx, "access_end", time.Since(start), reqBody, blw.body.String())
		}()
		// 继续处理请求
		ctx.Next()
	}
}

func accessLog(ctx *gin.Context,
	accessType string, dur time.Duration,
	body []byte, dataOut interface{}) {
	req := ctx.Request
	bodyStr := string(body)
	query := req.URL.RawQuery
	path := req.URL.Path

	// TODO: 实现Token认证后再把访问日志里也加上token记录

	logger.New(ctx).Info("AccessLog",
		"type", accessType,
		"ip", ctx.ClientIP(),
		"method", req.Method,
		"path", path,
		"query", query,
		"body", bodyStr,
		"output", dataOut,
		"time(ms)", int64(dur/time.Millisecond),
	)

}

// GinPanicRecovery 函数用于处理Gin框架中的panic错误
func GinPanicRecovery() gin.HandlerFunc {

	// 返回一个gin.HandlerFunc类型的函数
	return func(ctx *gin.Context) {
		// 使用defer关键字，在函数返回前执行
		defer func() {
			// 检查是否有panic错误
			if err := recover(); err != nil {
				// 定义一个布尔变量，用于判断是否是broken pipe错误
				var brokenPipe bool
				// 判断错误类型是否为net.OpError
				if ne, ok := err.(*net.OpError); ok {
					// 判断错误类型是否为os.SyscallError
					if se, ok := ne.Err.(*os.SyscallError); ok {
						// 定义两个字符串，用于判断错误信息是否包含broken pipe或connection reset by peer
						brkPipe := "broken pipe"
						conRByPerr := "connection reset by peer"
						// 将错误信息转换为小写
						seError := strings.ToLower(se.Error())
						// 判断错误信息是否包含broken pipe或connection reset by peer
						if strings.Contains(seError, brkPipe) ||
							strings.Contains(seError, conRByPerr) {
							// 如果包含，则将brokenPipe设置为true
							brokenPipe = true
						}
					}

				}
				// 将http请求转换为字符串
				httpRequest, _ := httputil.DumpRequest(ctx.Request, false)
				// 如果是broken pipe错误，则记录错误信息并返回错误
				if brokenPipe {
					logger.New(ctx).Error("http request broken pipe",
						"path", ctx.Request.URL.Path,
						"error", err,
						"request", string(httpRequest),
					)
					ctx.Error(err.(error))
					ctx.Abort()
					return
				}
				// 记录panic错误信息
				logger.New(ctx).Error("http_request_panic",
					"path", ctx.Request.URL.Path,
					"error", err,
					"request", string(httpRequest),
					"stack", string(debug.Stack()),
				)
				// 返回500错误
				ctx.AbortWithError(http.StatusInternalServerError, err.(error))
			}
		}()
		// 继续处理下一个中间件
		ctx.Next()
	}
}
